﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
using System.Linq.Expressions;

namespace Kocic.RepositoryManagement.LinqToSql
{
    /// <summary>
    /// Defines methods and operations for managers to perform create, read, update, and delete operations on a data object, in a data context, through Linq to SQL.
    /// </summary>
    /// <typeparam name="TDataObject">The type of the data object that the LINQ to SQL data manager operations will be implemented for.</typeparam>
    /// <typeparam name="TContext">The type of the data context that will be used by the LINQ to SQL data managers.</typeparam>
    public abstract class LinqToSqlRepositoryManager<TDataObject, TContext> : DataManager<TDataObject, TContext>, IRetrievable<TDataObject>
        where TDataObject : class
        where TContext : DataContext, new()
    {
        /// <summary>
        /// Creates an instance of the LinqToSqlDataManager class.
        /// </summary>
        /// <param name="context">An instance of the data context that will be used for operations.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected LinqToSqlRepositoryManager(TContext context)
            : base(context)
        {

        }

        /// <summary>
        /// Creates an instance of the LinqToSqlDataManager class. Invoking this constructor will pass the static instance reference to the data context referenced an instance of the LinqToSqlExecutionContext class.
        /// </summary>  
        protected LinqToSqlRepositoryManager()
            : this(LinqToSqlExecutionContext<TContext>.CurrentDataSource)
        {

        }

        /// <summary>
        /// Disposes unmanaged resources.
        /// </summary>
        /// <param name="disposing">Value indicating if to invoke the method from the IDisposable.Dispose implementation or from the finalizer.</param>
        protected override void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                if (this.IsDisposed)
                {
                    return;
                }

                /*Checks to see if the context was set in this fashion. If yes, then we do not dispose of the context yet */
                if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
                {
                    this.DisposeDataSource = false;
                }

                if (this.DisposeDataSource)
                {
                    if (this.DataSource.Connection.State == System.Data.ConnectionState.Open)
                    {
                        this.DataSource.Connection.Close();
                    }

                    this.DataSource.Dispose();
                }
                base.Dispose(disposing);
            }
        }

        /// <summary>
        /// Saves data object to data source. Creates new data or updates existing data.
        /// </summary>
        /// <param name="dataObject">The data object to save to the context and database.</param>
        /// <param name="method">The method of saving a data object to the context and database.</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public override void Save(TDataObject dataObject, SaveMethod method)
        {
            base.Save(dataObject, method);
        }

        /// <summary>
        /// Saves changes to the data context and commits them to the database.
        /// </summary>
        /// <param name="dataObject">Note: The data object is not required for this operation.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected override void Commit(TDataObject dataObject = null)
        {
            try
            {
                this.DataSource.SubmitChanges(ConflictMode.ContinueOnConflict);
            }
            catch (ChangeConflictException)
            {
                foreach (ObjectChangeConflict o in this.DataSource.ChangeConflicts)
                {
                    o.Resolve(RefreshMode.KeepChanges);
                }
            }
        }

        /// <summary>
        /// Performs operations to add the data object to the data context.
        /// </summary>
        /// <param name="dataObject">The data object to add.</param>
        protected override void DataSourceCreateOperations(TDataObject dataObject)
        {
            this.CurrentEntityTable.InsertOnSubmit(dataObject);
        }

        /// <summary>
        /// Performs operations to update the data object in the data context.
        /// </summary>
        /// <param name="dataObject">The data object to update.</param>
        protected override void DataSourceUpdateOperations(TDataObject dataObject)
        {
            //No update operations implemented since the data context automatically determines if the data
            //object has been updated.
        }

        /// <summary>
        /// Performs operations to remove the data object from the data context.
        /// </summary>
        /// <param name="dataObject">The data object to remove.</param>
        protected override void DataSourceDeleteOperations(TDataObject dataObject)
        {
            this.CurrentEntityTable.DeleteOnSubmit(dataObject);
        }

        /// <summary>
        /// Checks to see if a LinqToSqlContextDeferrer instance was instantiated and a reference exists.
        /// </summary>
        /// <returns>Value indicating if the LinqToSqlContextDeferrer was instantiated and a reference exists.</returns>
        protected override bool IsDeferralInstanceInstantiated()
        {
            return (LinqToSqlContextDeferrer<TContext>.DeferralOperation != null);
        }

        /// <summary>
        /// Executes any deferral operations for the provided data object.
        /// </summary>
        /// <param name="dataObject">The data object to defer operations on.</param>
        protected override void DeferrerOperations(TDataObject dataObject)
        {
            if (this.IsDeferralInstanceInstantiated())
            {
                LinqToSqlContextDeferrer<TContext>.DeferredObjects.Add(dataObject);
            }
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is created and added to the context.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to adding and saving the changes to the context.</param>
        protected override TDataObject PreCreate(TDataObject dataObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is updated and its changes saved to the context.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to saving its changes to the context.</param>
        protected override TDataObject PreUpdate(TDataObject dataObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is deleted and removed from the context.
        /// </summary>
        /// <param name="dataObject">The data object used in performing additional operations to prior to deleting it from the context and saving the changes.</param>
        protected override TDataObject PreDelete(TDataObject dataObject)
        {
            return null;
        }

        /// <summary>
        /// Gets the current entity table of type TDataObject.
        /// </summary>
        protected virtual Table<TDataObject> CurrentEntityTable
        {
            get
            {
                return this.DataSource.GetTable<TDataObject>();
            }
        }

        #region IRetrievable<TDataObject> Members

        /*These methods have been marked as abstract since the manager must be required to provide the basic read functionality */

        /// <summary>
        /// Defines how to return a single instance of the data object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>The data object that is a result of a search.</returns>
        public abstract TDataObject RetrieveSingle(Object searchValue);

        /// <summary>
        /// Defines how to retrieve multiple instances of the data object.
        /// </summary>
        /// <returns>An enumerator of a collection of type TDataObject.</returns>
        public abstract IEnumerable<TDataObject> RetrieveMultiple();

        /// <summary>
        /// Defines how to retrieve multiple instances of the data object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>An enumerator of a collection of type TDataObject that is the result of a search.</returns>
        public abstract IEnumerable<TDataObject> RetrieveMultiple(Object searchValue);

        #endregion

        /// <summary>
        /// Saves a data object to the context and the database. Either creates or updates the data object based on the provided save method. If an instance of the
        /// LinqToSqlContextDeferral instance exists, then the data object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="dataObject">The data object to save to the context and database.</param>
        /// <param name="method">The method of saving a data object.</param>
        /// <param name="context">A instance of the object context to which the entity object will be saved to. If this instance is null, then a object context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void SaveDataObject(TDataObject dataObject, SaveMethod method, TContext context = null)
        {
            if (method == SaveMethod.None)
            {
                throw new ArgumentException("Save method must be either Create or Update.", "method");
            }

            LinqToSqlRepositoryManager<TDataObject, TContext>.ContextOperations(dataObject, context, (inDataObject, inContext) =>
            {
                if (method == SaveMethod.Create)
                {
                    inContext.GetTable<TDataObject>().InsertOnSubmit(inDataObject);
                }
            });
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TEntityObject> object based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TDataObject> Retrieve(Expression<Func<TDataObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = LinqToSqlExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the LinqToSqlExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.GetTable<TDataObject>().Where(whereExpression);
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TEntityObject> object based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TDataObject> Retrieve(Expression<Func<TDataObject, TDataObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = LinqToSqlExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the LinqToSqlExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.GetTable<TDataObject>().Select(retrieveExpression);
        }

        /// <summary>
        /// Deletes data objects based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteDataObjects(Expression<Func<TDataObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = LinqToSqlExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the LinqToSqlExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.GetTable<TDataObject>().Where(whereExpression);

            foreach (var o in objects)
            {
                LinqToSqlRepositoryManager<TDataObject, TContext>.DeleteDataObject(o);
            }
        }

        /// <summary>
        /// Deletes data objects based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Select expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteDataObjects(Expression<Func<TDataObject, TDataObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = LinqToSqlExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the LinqToSqlExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.GetTable<TDataObject>().Select(retrieveExpression);

            foreach (var o in objects)
            {
                LinqToSqlRepositoryManager<TDataObject, TContext>.DeleteDataObject(o);
            }
        }

        /// <summary>
        /// Removes a data object from the data context and the database. If an instance of the
        /// LinqToSqlContextDeferrer instance exists, then the entity object is simply removed and no changes are saved.
        /// </summary>
        /// <param name="dataObject">The data object to delete and remove from the data context.</param>
        /// <param name="context">A instance of the data context to which the entity object will be removed from. If this instance is null, then a data context instance referenced
        /// by a LinqToSqlExecutionContext instance will be used. If this instance is not null and an instance of the LinqToSqlExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void DeleteDataObject(TDataObject dataObject, TContext context = null)
        {
            LinqToSqlRepositoryManager<TDataObject, TContext>.ContextOperations(dataObject, context, (inDataObject, inContext) => inContext.GetTable<TDataObject>().DeleteOnSubmit(dataObject));
        }

        /// <summary>
        /// Helper method which has the same logic when trying to add/remove entity objects in/from a context. Because the same logic is used, a delegate to the context's
        /// AddObject/DeleteObject methods is used.
        /// </summary>
        /// <param name="dataObject">The entity object for which the appropriate action will take place.</param>
        /// <param name="context">The object context for which the action upon the entity object will take place.</param>
        /// <param name="contextAction">A delegate to a method that will execute the appropriate context operations.</param>
        private static void ContextOperations(TDataObject dataObject, TContext context, Action<TDataObject, TContext> contextAction)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = LinqToSqlExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && LinqToSqlExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided data context in one of the arguments, since an instance of the LinqToSqlExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not bee instantiated.");
            }

            bool toDefer = LinqToSqlContextDeferrer<TContext>.DeferralOperation != null ? true : false;

            try
            {
                contextAction(dataObject, currentContext);

                if (!toDefer)
                {
                    currentContext.SubmitChanges(ConflictMode.ContinueOnConflict);
                }
                else
                {
                    LinqToSqlContextDeferrer<TContext>.DeferredObjects.Add(dataObject);
                }
            }
            catch (ChangeConflictException)
            {
                foreach (ObjectChangeConflict o in currentContext.ChangeConflicts)
                {
                    o.Resolve(RefreshMode.KeepChanges);
                }
                currentContext.SubmitChanges();
            }
        }
    }
}
